﻿using System;
using System.Reflection;
using Castle.Components.Binder;
using Castle.MonoRail.Framework;
using NHibernate;
using Yama.Util.CodeContracts;

namespace Yama.MVC.NHibernate.DataBinder
{
    public class NHDataBindFromKeyAttribute : Attribute, IParameterBinder
    {
        private string _formField;
        private readonly string _primaryKeyProperty;
        private ParamSource _from = ParamSource.Params;

        /// <summary>
        /// Initializes a new instance of NHDataBindFromKeyAttribute, which can bind a
        /// persistent object from primary key
        /// Will use parameter name with "Id" appended to look for primary key value.
        /// 
        /// For example if parameter is called user, will look for a userId value in selected
        /// <see cref="ParamSource"/>
        /// </summary>
        public NHDataBindFromKeyAttribute()
        {
        }

        /// <summary>
        /// Initializes a new instance of NHDataBindFromKeyAttribute, which can bind a
        /// persistent object from primary key or (if IsArray is set) an array of objects 
        /// from flat nodes (ie. object[0], object[1]...)
        /// 
        /// Will use <param name="formField" /> to look for primary key value in selected
        /// <see cref="ParamSource" />
        /// </summary>
        public NHDataBindFromKeyAttribute(string formField)
            : this()
        {
            _formField = formField;
        }

        /// <summary>
        /// Initializes a new instance of NHDataBindFromKeyAttribute, which can bind an
        /// array of persistent objects form non flat node (ie. object[0].Id, object[1].Id
        /// 
        /// It is not necessary to set is array with this constructor
        /// </summary>
        /// <param name="formField">Name of array in form. An example is the "object" part in object[0].Id</param>
        /// <param name="primaryKeyProperty">Name of primary key. An example is the "Id" part in object[0].Id</param>
        public NHDataBindFromKeyAttribute(string formField, string primaryKeyProperty)
            : this(formField)
        {
            _primaryKeyProperty = primaryKeyProperty;
            IsArray = true;
            NonFlatNodes = true;
        }

        /// <summary>
        /// By default will look for parameter in Form, Querystring and Flash
        /// </summary>
        public ParamSource From
        {
            get { return _from; }
            set { _from = value; }
        }

        /// <summary>
        /// Gets or sets if instance should be fetched using <see cref="ISession"/>#Load or
        /// <see cref="ISession"/>#Get. Default is to fetch by Get
        /// </summary>
        public bool FetchByLoad { get; set; }

        /// <summary>
        /// Instructs data binder to catch any binding exceptions 
        /// and return null in case of binding errors
        /// </summary>
        public bool SuppressErrors { get; set; }

        /// <summary>
        /// Will bind an array of persistent objects
        /// </summary>
        public bool IsArray { get; set; }

        private string ArraySuffix
        {
            get
            {
                return string.IsNullOrEmpty(_primaryKeyProperty)
                    ? string.Empty
                    : string.Format(".{0}", _primaryKeyProperty);
            }
        }

        private bool NonFlatNodes { get; set; }

        public int CalculateParamPoints(IEngineContext context, IController controller, IControllerContext controllerContext, ParameterInfo parameterInfo)
        {
            EnsurePrimaryKey(parameterInfo);

            object keyValue = GetPrimaryKey(context, From, IsArray
                ? string.Format("{0}[0]{1}", _formField, ArraySuffix)
                : _formField);

            return keyValue == null ? 0 : 10;
        }

        private void EnsurePrimaryKey(ParameterInfo parameterInfo)
        {
            if (string.IsNullOrEmpty(_formField))
            {
                if (IsArray)
                    _formField = parameterInfo.Name;
                else
                    _formField = parameterInfo.Name + "Id";
            }
        }

        private static object GetPrimaryKey(IEngineContext context, ParamSource paramSource, string primaryKey)
        {
            object value = null;
            switch (paramSource)
            {
                case ParamSource.Form:
                    value = context.Request.Form[primaryKey];
                    break;
                case ParamSource.QueryString:
                    value = context.Request.QueryString[primaryKey];
                    break;
                case ParamSource.Flash:
                    value = context.Flash[primaryKey];
                    break;
                case ParamSource.Params:
                    value = context.Request.Form[primaryKey];

                    if (value == null)
                        value = context.Request.QueryString[primaryKey];

                    if (value == null)
                        value = context.Flash[primaryKey];
                    break;
            }

            return value;
        }

        public object Bind(IEngineContext context, IController controller, IControllerContext controllerContext, ParameterInfo parameterInfo)
        {
            EnsurePrimaryKey(parameterInfo);

            Check.Require(controller is SmartDispatcherController, "Controller must inherit from SmartDispatcherController");
            SmartDispatcherController ctrl = (SmartDispatcherController)controller;

            Check.Require(ctrl.Binder is NhibernateDataBinder, "Controller's Binder must be set to NhibernateDataBinder instance");
            NhibernateDataBinder binder = (NhibernateDataBinder)ctrl.Binder;
            try
            {
                var boundObject = binder.BindPersistentObject(
                    parameterInfo.ParameterType,
                    _formField, ObtainNode(context),
                    this.FetchByLoad,
                    this.NonFlatNodes);

                controllerContext.PropertyBag[parameterInfo.Name] = boundObject;

                return boundObject;
            }
            catch
            {
                if (SuppressErrors)
                    return null;

                throw;
            }
        }

        private CompositeNode ObtainNode(IEngineContext context)
        {
            switch (this.From)
            {
                case ParamSource.Form:
                    return context.Request.ObtainParamsNode(ParamStore.Form);
                case ParamSource.QueryString:
                    return context.Request.ObtainParamsNode(ParamStore.QueryString);
                case ParamSource.Flash:
                    return CreateParamsNodeFromFlash(context);
                default:
                    var node = context.Request.ObtainParamsNode(ParamStore.Params);
                    var flashNodes = CreateParamsNodeFromFlash(context);

                    foreach (var childNode in flashNodes.ChildNodes)
                    {
                        node.AddChildNode(childNode);
                    }

                    return node;
            }
        }

        private static CompositeNode CreateParamsNodeFromFlash(IEngineContext context)
        {
            var root = new CompositeNode("root");
            var flash = context.Flash;

            foreach (var flashKey in flash.Keys)
            {
                root.AddChildNode(new LeafNode(flash[flashKey].GetType(), flashKey.ToString(), flash[flashKey]));
            }

            return root;
        }
    }
}